<!--
    by: yuanbiao 2020年09月27日
    name: x-drag-item
    notes:
-->
<template>
  <div ref="dragItem" :class="[prefixCls]"
    :draggable="false"
    :style="itemStyle"
    @touchstart="touchstartHandler($event,'move')"
    @mousedown="mousedownHandler($event,'move')"
    @dragstart.stop.prevent
  >
    <div :class="`${prefixCls}__content`">
      <slot></slot>
    </div>
    <div :class="`${prefixCls}__controls`" v-show="controls">
      <!-- <div :class="[`${prefixCls}__controls_circle`, `${prefixCls}__controls_circle_lt`]"></div>
      <div :class="[`${prefixCls}__controls_circle`, `${prefixCls}__controls_circle_rt`]"></div>
      <div
        :class="[`${prefixCls}__controls_circle`, `${prefixCls}__controls_circle_lb`, {
          [`${prefixCls}__controls_circle_rotate`]: rotatable
        }]"
        @touchstart.stop="touchstartHandler($event,'rotate')"
        @mousedown.stop="mousedownHandler($event,'rotate')"
      >
        <i v-show="rotatable" class="xvu-iconfont xvu-rotate"></i>
      </div>
      <div
        :class="[`${prefixCls}__controls_circle`, `${prefixCls}__controls_circle_rb`, {
          [`${prefixCls}__controls_circle_resize`]: resizable
        }]"
        @touchstart.stop="touchstartHandler($event,'resize')"
        @mousedown.stop="mousedownHandler($event,'resize')"
      >
        <i v-show="resizable" class="xvu-iconfont xvu-resize"></i>
      </div> -->
      <div
        :class="[`${prefixCls}__controls_corner`, `${prefixCls}__controls_corner_rt`, {
          [`${prefixCls}__controls_corner_rotate`]: rotatable
        }]"
        @touchstart.stop="touchstartHandler($event,'rotate')"
        @mousedown.stop="mousedownHandler($event,'rotate')"
      >
        <i v-show="rotatable" class="xvu-iconfont xvu-rotate-2"></i>
      </div>
      <div
        :class="[`${prefixCls}__controls_corner`, `${prefixCls}__controls_corner_rb`, {
          [`${prefixCls}__controls_corner_resize`]: resizable
        }]"
        @touchstart.stop="touchstartHandler($event,'resize')"
        @mousedown.stop="mousedownHandler($event,'resize')"
      >
        <i v-show="resizable" class="xvu-iconfont xvu-resize-2"></i>
      </div>
    </div>
  </div>
</template>

<script>
const prefixCls = 'x-drag-item'
export default {
  name: 'x-drag-item',
  props: {
    options: {
      type: Object,
      default () {
        return {
          width: 100, height: 100,
          x: 0, y: 0,
          'x%': '0%', 'y%': '0%',
          fontSize: 14, unit: 'px',
          scale: 1, rotate: 0,
          controls: false,
          resizable: false,
          rotatable: false,
          dynamic: false,
          scope: false
        }
      }
    }
  },
  data() {
    return {
      prefixCls: prefixCls,
      operationType: '',
      userOperationType: '',
      cursorStyle: 'pointer',
      dynamic: false, // 容器大小自适应
      scope: false, // 是否只允许在特定范围内拖拽
      controls: false, // 是否显示控制边框
      resizable: false, // 是否可缩放
      rotatable: false, // 是否可旋转
      dragAreaInfo: {
        width: 0, height: 0,
        x: 0, y: 0,
        xMin: 0, yMin: 0,
        xMax: 0, yMax: 0
      },
      dragItemInfo: {
        width: 0, height: 0,
        width_: 0, height_: 0,
        'width%': '0%', 'height%': '0%',
        fontSize: 14, unit: 'px',
        'x%': '0%', 'y%': '0%',
        x: 0, y: 0,
        x_: 0, y_: 0,
        x0: 0, y0: 0,
        x1: 0, y1: 0,
        cx: 0, cy: 0,
        scale: 1,
        rotate: 0, rotate_: 0,
        angle0: 0, angle1: 0,
        dist0: 0, dist1: 0
      }
    }
  },
  computed: {
    itemStyle () {
      let {width, height, x, y, fontSize, scale, rotate} = this.dragItemInfo
      let _width = this.dynamic ? 'auto' : `${width * scale}px`
      let _height = this.dynamic ? 'auto' : `${height * scale}px`
      let _left = (this.dragItemInfo.unit === '%') ? this.dragItemInfo['x%'] : `${x}px`
      let _top = (this.dragItemInfo.unit === '%') ? this.dragItemInfo['y%'] : `${y}px`
      return {
        width: _width,
        height: _height,
        left: _left,
        top: _top,
        fontSize: `${fontSize * scale}px`,
        transform: `rotate(${rotate}deg)`,
        cursor: this.cursorStyle
      }
    },
    mark () {
      let result = [
        this.dragItemInfo['x%'], this.dragItemInfo['y%'],
        this.dragItemInfo['width%'], this.dragItemInfo['height%'],
        this.dragItemInfo['rotate']
      ].join('_')
      return result
    }
  },
  watch: {
    options: {
      handler (n) {
        this.mergeOptions(n)
      },
      // immediate: true,
      deep: true
    },
    mark (n, o) {
      if (n !== o) {
        this.$emit('change', this.getItemInfo())
      }
    }
  },
  mounted () {
    this.$nextTick(() => {
      this.getDragAreaInfo()
      this.mergeOptions(this.options)
    })
  },
  methods: {
    mergeOptions (data) {
      data = data || {}
      let _unit = data.unit || this.dragItemInfo.unit || 'px'
      Object.keys(data).forEach(k => {
        let _k = k.replace(/%/g, '')
        switch (k) {
          case 'width':
          case 'height':
          case 'width%':
          case 'height%':
            if (/%$/.test(data[k])) {
              if (data[k] !== this.dragItemInfo[`${_k}%`]) {
                this.$set(this.dragItemInfo, _k, this.per2px(data[k], this.dragAreaInfo[_k]))
                this.$set(this.dragItemInfo, `${_k}%`, data[k])
              }
            } else {
              let _v = parseInt(data[k]) || 0
              if (_v !== this.dragItemInfo[_k]) {
                this.$set(this.dragItemInfo, _k, _v)
                this.$set(this.dragItemInfo, `${_k}%`, this.px2per(_v, this.dragAreaInfo[_k]))
              }
            }
            break
          case 'x':
          case 'y':
          case 'x%':
          case 'y%':
            if (/%$/.test(data[k])) {
              if (data[k] !== this.dragItemInfo[`${_k}%`]) {
                let _v = parseFloat(data[k]) || 0
                _v = (_unit === '%' ? _v : this.per2px(`${_v}%`, this.dragAreaInfo[_k === 'x' ? 'width' : 'height']))
                this.$set(this.dragItemInfo, _k, _v)
                this.$set(this.dragItemInfo, `${_k}%`, `${_v}%`)
              }
            } else {
              let _v = parseInt(data[k]) || 0
              if (_v !== this.dragItemInfo[_k]) {
                this.$set(this.dragItemInfo, _k, _v)
                this.$set(this.dragItemInfo, `${_k}%`, this.px2per(_v, this.dragAreaInfo[_k === 'x' ? 'width' : 'height']))
              }
            }
            break
          case 'fontSize':
          case 'scale':
          case 'rotate':
            this.$set(this.dragItemInfo, k, parseFloat(data[k]) || 0)
            break
          case 'controls':
          case 'resizable':
          case 'rotatable':
          case 'dynamic':
          case 'scope':
            this[k] = !!data[k]
            break
          case 'unit':
            this.$set(this.dragItemInfo, k, data[k] || 'px')
            break
        }
      })
    },
    touchstartHandler (e, operationType) {
      this.operationType = operationType
      this.dragItemInfo.x1 = this.dragItemInfo.x0 = e.touches[0].clientX
      this.dragItemInfo.y1 = this.dragItemInfo.y0 = e.touches[0].clientY
      this.operationBefore()
      window.addEventListener('touchmove', this.touchmoveHandler)
      window.addEventListener('touchend', this.touchendHandler)
    },
    mousedownHandler (e, operationType) {
      this.operationType = operationType
      this.dragItemInfo.x1 = this.dragItemInfo.x0 = e.clientX
      this.dragItemInfo.y1 = this.dragItemInfo.y0 = e.clientY
      this.operationBefore()
      window.addEventListener('mousemove', this.mousemoveHandler)
      window.addEventListener('mouseup', this.mouseupHandler)
      window.addEventListener('mouseleave', this.mouseupHandler)
    },
    touchmoveHandler (e) {
      if (this.operationType && e.touches[0]) {
        this.dragItemInfo.x1 = e.touches[0].clientX
        this.dragItemInfo.y1 = e.touches[0].clientY
        this.operationProcess()
      }
    },
    mousemoveHandler (e) {
      if (this.operationType) {
        this.dragItemInfo.x1 = e.clientX
        this.dragItemInfo.y1 = e.clientY
        this.operationProcess()
      }
    },
    touchendHandler () {
      this.operationAfter()
      this.operationType = ''
      window.removeEventListener('touchmove', this.touchmoveHandler)
      window.removeEventListener('touchend', this.touchendHandler)
    },
    mouseupHandler () {
      this.operationAfter()
      this.operationType = ''
      window.removeEventListener('mousemove', this.mousemoveHandler)
      window.removeEventListener('mouseup', this.mouseupHandler)
      window.removeEventListener('mouseleave', this.mouseupHandler)
    },
    getDragAreaInfo () {
      if (!this.$refs.dragItem) return
      let dragItem = this.$refs.dragItem || this.$el
      let dragArea = dragItem.closest('.x-drag') || dragItem.parentNode
      // let dragItemRect = dragItem.getBoundingClientRect()
      // let fontSize = parseInt(getComputedStyle(dragItem).fontSize)
      // let rotate = this.safeNumber(this.dragItemInfo.rotate)
      // dragItemInfo
      // this.$set(this.dragItemInfo, 'fontSize', fontSize)
      // this.$set(this.dragItemInfo, 'width', dragItemRect.width)
      // this.$set(this.dragItemInfo, 'height', dragItemRect.height)
      // this.$set(this.dragItemInfo, 'x', dragItemRect.x)
      // this.$set(this.dragItemInfo, 'y', dragItemRect.y)
      // dragAreaInfo
      if (!dragArea) return
      const dragAreaRect = dragArea.getBoundingClientRect()
      this.dragAreaInfo.x = dragAreaRect.x || dragAreaRect.left
      this.dragAreaInfo.y = dragAreaRect.y || dragAreaRect.top
      this.dragAreaInfo.width = dragAreaRect.width
      this.dragAreaInfo.height = dragAreaRect.height
      this.dragAreaInfo.xMin = 0
      this.dragAreaInfo.yMin = 0
      this.dragAreaInfo.xMax = dragAreaRect.width
      this.dragAreaInfo.yMax = dragAreaRect.height
      // 修正 dragItem 坐标
      // if (this.dragItemInfo.unit === '%') {
      //   this.$set(this.dragItemInfo, 'x', dragItemRect.x - this.dragAreaInfo.x)
      //   this.$set(this.dragItemInfo, 'y', dragItemRect.y - this.dragAreaInfo.y)
      //   this.$set(this.dragItemInfo, 'width%', this.px2per(dragItemRect.width, dragAreaInfo.width))
      //   this.$set(this.dragItemInfo, 'height%', this.px2per(dragItemRect.height, dragAreaInfo.height))
      // }
    },
    updateDragAreaInfo () {
      if (!this.$refs.dragItem) return
      const dragItem = this.$refs.dragItem || this.$el
      const dragArea = dragItem.closest('.x-drag') || dragItem.parentNode
      const dragItemRect = dragItem.getBoundingClientRect()
      if (!dragArea) return
      const dragAreaRect = dragArea.getBoundingClientRect()
      const rotate = this.safeNumber(this.dragItemInfo.rotate)
      this.dragAreaInfo.x = dragAreaRect.x || dragAreaRect.left
      this.dragAreaInfo.y = dragAreaRect.y || dragAreaRect.top
      this.dragAreaInfo.width = dragAreaRect.width
      this.dragAreaInfo.height = dragAreaRect.height
      if (rotate === 0) {
        this.$set(this.dragItemInfo, 'x', dragItemRect.x - this.dragAreaInfo.x)
        this.$set(this.dragItemInfo, 'y', dragItemRect.y - this.dragAreaInfo.y)
        this.$set(this.dragItemInfo, 'width', dragItemRect.width)
        this.$set(this.dragItemInfo, 'height', dragItemRect.height)
      }
      if (this.dragItemInfo.unit === '%') {
        // 修正 dragItem 坐标、宽高
        this.$set(this.dragItemInfo, 'x', this.per2px(this.dragItemInfo['x%'], dragAreaRect.width))
        this.$set(this.dragItemInfo, 'y', this.per2px(this.dragItemInfo['y%'], dragAreaRect.height))
        this.$set(this.dragItemInfo, 'width', this.per2px(this.dragItemInfo['width%'], dragAreaRect.width))
        this.$set(this.dragItemInfo, 'height', this.per2px(this.dragItemInfo['height%'], dragAreaRect.height))
      }
      this.dragAreaInfo.xMin = Math.min(dragAreaRect.width - this.dragItemInfo.width, 0)
      this.dragAreaInfo.yMin = Math.min(dragAreaRect.height - this.dragItemInfo.height, 0)
      this.dragAreaInfo.xMax = dragAreaRect.width - this.dragItemInfo.width
      this.dragAreaInfo.yMax = dragAreaRect.height - this.dragItemInfo.height
    },
    operationBefore () {
      if (!this.$refs.dragItem) return
      this.updateDragAreaInfo()
      this.dragItemInfo.x_ = this.dragItemInfo.x
      this.dragItemInfo.y_ = this.dragItemInfo.y
      this.dragItemInfo.width_ = this.dragItemInfo.width
      this.dragItemInfo.height_ = this.dragItemInfo.height
      this.dragItemInfo.rotate_ = this.dragItemInfo.rotate
      this.dragItemInfo.cx = this.dragItemInfo.x + this.dragItemInfo.width / 2
      this.dragItemInfo.cy = this.dragItemInfo.y + this.dragItemInfo.height / 2
      // 计算初始距离和角度，用于旋转和缩放操作
      this.dragItemInfo.dist0 = Math.sqrt(
        Math.pow(this.dragItemInfo.x0 - this.dragItemInfo.cx, 2) +
        Math.pow(this.dragItemInfo.y0 - this.dragItemInfo.cy, 2)
      )
      this.dragItemInfo.angle1 = this.dragItemInfo.angle0 = this.getAngleByPoint(
        {x: this.dragItemInfo.cx, y: this.dragItemInfo.cy},
        {x: this.dragItemInfo.x0, y: this.dragItemInfo.y0}
      )
    },
    operationProcess () {
      let rotate = this.safeNumber(this.dragItemInfo.rotate)
      if (this.resizable && this.operationType === 'resize') {
        // this.dragItemInfo.dist1 = Math.sqrt(Math.pow(this.dragItemInfo.x1 - this.dragItemInfo.x, 2) + Math.pow(this.dragItemInfo.y1 - this.dragItemInfo.y, 2))
        // this.$set(this.dragItemInfo, 'scale', this.dragItemInfo.dist1 / this.dragItemInfo.dist0)
        if (this.scope) {
          let x1 = Math.min(Math.max(this.dragItemInfo.x1, 0), this.dragAreaInfo.x + this.dragAreaInfo.width)
          let y1 = Math.min(Math.max(this.dragItemInfo.y1, 0), this.dragAreaInfo.y + this.dragAreaInfo.height)
          this.$set(this.dragItemInfo, 'x1', x1)
          this.$set(this.dragItemInfo, 'y1', y1)
        }
        let width = Math.max(this.dragItemInfo.x1 - this.dragItemInfo.x - this.dragAreaInfo.x, 0)
        let height = Math.max(this.dragItemInfo.y1 - this.dragItemInfo.y - this.dragAreaInfo.y, 0)
        if (rotate !== 0) {
          let point0 = {x: this.dragItemInfo.x, y: this.dragItemInfo.y}
          let point1 = {
            x: this.dragItemInfo.x1 - this.dragAreaInfo.x,
            y: this.dragItemInfo.y1 - this.dragAreaInfo.y
          }
          let dx = point1.x - point0.x
          let dy = point1.y - point0.y
          let d = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2))
          let angle0 = rotate * Math.PI / 180
          let angle1 = this.getAngleByPoint(point0, point1)
          let angle2 = (angle1 - angle0 + Math.PI * 2) % (Math.PI * 2)
          width = Math.abs(d * Math.cos(angle2))
          height = Math.abs(d * Math.sin(angle2))
        }
        this.$set(this.dragItemInfo, 'width', width)
        this.$set(this.dragItemInfo, 'height', height)
        this.$set(this.dragItemInfo, 'width%', this.px2per(width, this.dragAreaInfo.width))
        this.$set(this.dragItemInfo, 'height%', this.px2per(height, this.dragAreaInfo.height))
      } else if (this.rotatable && this.operationType === 'rotate') {
        this.dragItemInfo.angle1 = this.getAngleByPoint(
          {x: this.dragItemInfo.cx, y: this.dragItemInfo.cy},
          {x: this.dragItemInfo.x1, y: this.dragItemInfo.y1}
        )
        this.$set(this.dragItemInfo, 'rotate', this.dragItemInfo.rotate_ + (this.dragItemInfo.angle1 - this.dragItemInfo.angle0) * 180 / Math.PI)
      } else {
        // update cursor
        let dx = this.dragItemInfo.x1 - this.dragItemInfo.x0
        let dy = this.dragItemInfo.y1 - this.dragItemInfo.y0
        let d = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2))
        if (d > 5) {
          this.cursorStyle = 'move'
        }
        // update x,y,x%,y%
        let x = this.dragItemInfo.x_ + dx
        let y = this.dragItemInfo.y_ + dy
        if (this.scope) {
          x = Math.max(x, this.dragAreaInfo.xMin)
          y = Math.max(y, this.dragAreaInfo.yMin)
          x = Math.min(x, this.dragAreaInfo.xMax)
          y = Math.min(y, this.dragAreaInfo.yMax)
        }
        this.$set(this.dragItemInfo, 'x', x)
        this.$set(this.dragItemInfo, 'y', y)
        this.$set(this.dragItemInfo, 'x%', this.px2per(x, this.dragAreaInfo.width))
        this.$set(this.dragItemInfo, 'y%', this.px2per(y, this.dragAreaInfo.height))
      }
    },
    operationAfter () {
      this.cursorStyle = 'pointer'
      if (this.resizable && this.operationType === 'resize') {
        if (!this.$refs.dragItem) return
        // let fontSize = parseInt(getComputedStyle(this.$refs.dragItem).fontSize)
        // this.$set(this.dragItemInfo, 'fontSize', fontSize)
        // this.$set(this.dragItemInfo, 'width', this.dragItemInfo.width * this.dragItemInfo.scale)
        // this.$set(this.dragItemInfo, 'height', this.dragItemInfo.height * this.dragItemInfo.scale)
        // this.$set(this.dragItemInfo, 'scale', 1)
        this.dragAreaInfo.xMin = Math.min(this.dragAreaInfo.width - this.dragItemInfo.width, 0)
        this.dragAreaInfo.yMin = Math.min(this.dragAreaInfo.height - this.dragItemInfo.height, 0)
        this.dragAreaInfo.xMax = this.dragAreaInfo.width - this.dragItemInfo.width
        this.dragAreaInfo.yMax = this.dragAreaInfo.height - this.dragItemInfo.height
      }
      if (this.operationType === 'move') {
        let dx = this.dragItemInfo.x1 - this.dragItemInfo.x0
        let dy = this.dragItemInfo.y1 - this.dragItemInfo.y0
        let d = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2))
        if (d < 5) {
          this.$emit('tap', this.getItemInfo())
        }
      }
    },
    getAngleByPoint (point0, point1) {
      let result = 0
      if (point0 && point1) {
        let dist = Math.sqrt(Math.pow(point1.x - point0.x, 2) + Math.pow(point1.y - point0.y, 2))
        if (point1.y >= point0.y && point1.x >= point0.x) {
          // 0 - Math.PI/2
          result = Math.asin((point1.y - point0.y) / dist) || 0
        } else if (point1.y >= point0.y && point1.x < point0.x) {
          // Math.PI/2 - Math.PI
          result = Math.PI - (Math.asin((point1.y - point0.y) / dist) || 0)
        } else if (point1.y < point0.y && point1.x < point0.x) {
          // Math.PI - Math.PI*3/2
          result = Math.PI + (Math.asin((point0.y - point1.y) / dist) || 0)
        } else if (point1.y < point0.y && point1.x >= point0.x) {
          // Math.PI*3/2 - Math.PI*2
          result = Math.PI * 2 - (Math.asin((point0.y - point1.y) / dist) || 0)
        }
      }
      return result
    },
    // 计算旋转变换后的点坐标
    getRotatedPoint(x, y, cx, cy, angle) {
      const rad = (angle || 0) * Math.PI / 180
      const cos = Math.cos(rad)
      const sin = Math.sin(rad)
      const nx = (x - cx) * cos - (y - cy) * sin + cx
      const ny = (x - cx) * sin + (y - cy) * cos + cy
      return { x: nx, y: ny }
    },
    // 安全的数值转换
    safeNumber(value, defaultValue = 0) {
      const num = parseFloat(value)
      return isNaN(num) ? defaultValue : num
    },
    px2per (px, total) {
      let result = 0
      px = parseFloat(px)
      if (!isNaN(px)) {
        result = ((px / total || 0) * 100).toFixed(1)
      }
      return `${result}%`
    },
    per2px (per, total) {
      let result = 0
      per = parseFloat(String(per).split('%')[0])
      if (!isNaN(per)) {
        result = parseInt(total * per / 100)
      }
      return result
    },
    getItemInfo () {
      let {width, height, x, y, fontSize, scale, rotate} = this.dragItemInfo
      return {
        operationType: this.operationType,
        width, height, x, y, fontSize, scale, rotate, mark: this.mark,
        'x%': this.dragItemInfo['x%'],
        'y%': this.dragItemInfo['y%'],
        'width%': this.dragItemInfo['width%'],
        'height%': this.dragItemInfo['height%']
      }
    }
  }
}
</script>
